/* ************************************************************************ */
/*                                                                          */
/*  Copyright (c)2005 Russell Weir | damonsbane _at_ gmail                  */
/*                                                                          */
/* This library is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU Lesser General Public               */
/* License as published by the Free Software Foundation; either             */
/* version 3.0 of the License, or (at your option) any later version.       */
/*                                                                          */
/* This library is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        */
/* Lesser General Public License or the LICENSE file for more details.      */
/*                                                                          */
/* ************************************************************************ */

package tzparser;

import haxe.io.Bytes;

#if neko

enum ArgState {
	NONE;
	START;
	END;
	SPECFILE;
}

class Parser {
	static var BUILDROOT 			: String		= "./tzparsertemp";
	static var TARGETROOT			: String		= "./tz";
	static var DATAROOT				: String		= "./tzparser/data";
	static var STATICSROOT			: String		= "./tzparser/static";
	static var STATICS				: Array<String>	= ["Zone.hx", "ChangeRecord.hx"];
	static var filter				: Array<String>; // hold the only files to generate
	static var _instance			: Parser;
	public var rules(default,null)	: Hash<RuleSet>;
	public var aliases(default,null): Hash<Array<AliasZone>>;
	public var zones(default,null)	: Hash<Zone>;

	static public function usage() {
		neko.Lib.println("Parser start 1850 end 2050 file /path/to/filter");
		neko.Sys.exit(0);
	}

	static public function main() {
		filter = new Array();
		if(!neko.FileSystem.exists("tz") || !neko.FileSystem.isDirectory("tz") ||
			!neko.FileSystem.exists("tzparser") || !neko.FileSystem.isDirectory("tzparser")
			) {
			neko.Lib.println("This program must be run in the root of haxealtdate");
			neko.Sys.exit(1);
		}

		if(neko.Sys.args().length > 0) {
			var args = neko.Sys.args();
			if(args[0] == "clean") {
				if(neko.FileSystem.exists(BUILDROOT))
					emptyDirectory(BUILDROOT, true);
				neko.Lib.println("Cleaning destination directory...");
				emptyDirectory(TARGETROOT+"/zones", true);
				emptyDirectory(TARGETROOT,false);
				neko.Sys.exit(0);
			}
			var state : ArgState = NONE;
			while(args.length > 0) {
				var arg = args.shift();
				switch (state) {
				case NONE:
					if(arg == "start") {
						state = START;
						continue;
					}
					if(arg == "end") {
						state = END;
						continue;
					}
					if(arg == "file") {
						neko.Lib.println("WARNING: the filter file functionality is not complete yet.");
						state = SPECFILE;
						continue;
					}
					if(arg == "clean") {
						neko.Lib.println("If you wish to clean the output directories, this must be the only command specified");
						neko.Sys.exit(1);
					}
					usage();
				case START:
					Params.year_min = Std.parseInt(arg);
					if(Params.year_min < 1700 || Params.year_min > 2000) {
						neko.Lib.println("Starting year must fall between 1700 and 2000, inclusive.");
						neko.Sys.exit(1);
					}
				case END:
					Params.year_max = Std.parseInt(arg);
					if(Params.year_max < 1700 || Params.year_max > 2100) {
						neko.Lib.println("Ending year must fall between 1700 and 2100, inclusive.");
						neko.Sys.exit(1);
					}
				case SPECFILE:
					if(!neko.FileSystem.exists(arg)) {
						neko.Lib.println("Specified file does not exist.");
						neko.Sys.exit(1);
					}
					var fi = neko.io.File.read(arg, false);
					var input : String = null;
					try {
						while(true) {
							input = StringTools.trim(fi.readLine());
							if(input.length > 0)
								filter.push(input);
							input = "";
						}
					}
					catch(e:haxe.io.Eof) {
						if(input.length > 0)
							filter.push(input);
					}
					catch(e:Dynamic) {
						neko.Lib.println("Error reading specified file.");
						neko.Sys.exit(1);
					}
					fi.close();
				}
			}
		}
		if(Params.year_min >= Params.year_max) {
			neko.Lib.println("Starting year must be less than the ending year.");
			usage();
		}

		if(!neko.FileSystem.exists(BUILDROOT))
			neko.FileSystem.createDirectory(BUILDROOT);
		emptyDirectory(BUILDROOT, true);

		
		var entries = new Array<String>();	
		var files = neko.FileSystem.readDirectory(DATAROOT);
		for(i in files) {
			neko.Lib.println("Parsing "+i+"...");
			// ignore any files with extensions
			if(i.indexOf(".") >= 0)
				continue;
			// ignore solar*
			if(i.indexOf("solar") >= 0)
				continue;
			var p = new Parser();

			var rv : {r:Bool,s:Array<String>} = p.parseFile(i);
			if(!rv.r) {
				neko.Lib.println("Error parsing file " + DATAROOT +"/"+ i);
				cleanup();
				neko.Sys.exit(1);
			}
			entries = entries.concat(rv.s);
		}

		var sb = new StringBuf();
		var sbfuncs = new StringBuf();
		sb.add(TimeZoneHeader);
		var start = true;
		var idx = 0;
		var count = 0;
		while(idx < entries.length) {
			if(start) {
				sb.add("\t\tinits_"+Std.string(count)+"();\n");
				sbfuncs.add("\n\tstatic function inits_"+Std.string(count)  +"() {\n");
				start = false;
			}
			sbfuncs.add("\t\t" + entries[idx] + "\n");
			if((idx%15 == 0 && idx != 0) || idx == entries.length -1) {
				sbfuncs.add("\t}\n");
				count++;
				start = true;
			}
			idx++;
		}
		sb.add("\t\tinitialized = true;\n\t}\n");
		sb.add(sbfuncs.toString());
		sb.add(TimeZoneFooter);
		
		neko.Lib.println("Creating Timezone.hx...");
		try {
			var fo = neko.io.File.write(BUILDROOT+"/Timezone.hx", false);
			fo.write(Bytes.ofString(sb.toString()));
			fo.close();
		}
		catch(e:Dynamic) {
			neko.Lib.println("Error creating "+BUILDROOT+"/Timezone.hx");
			neko.Lib.println(e);
		}

		// copy files over
		neko.Lib.println("Cleaning destination directory...");
		emptyDirectory(TARGETROOT+"/zones", true);
		emptyDirectory(TARGETROOT);
		neko.Lib.println("Installing new timezone files...");
		copyBuildToTarget("");

		cleanup();
		neko.Lib.println("Complete.");
	}

	static function emptyDirectory(p:String,?recurse:Bool) {
		var files = neko.FileSystem.readDirectory(p);
		if(recurse == null) recurse = false;
		for(i in files) {
			if(neko.FileSystem.isDirectory(p+"/"+i) && recurse == true) {
				if(i.charAt(0) == ".") {
					continue;
				}
				emptyDirectory(p+"/"+i, recurse);
				try {
					neko.FileSystem.deleteDirectory(p+"/"+i);
				}
				catch(e:Dynamic) {
					neko.Lib.println("Error removing directory "+p+"/"+i);
					//neko.Lib.rethrow(e);
				}
				continue;
			}
			var r = ~/\.hx$/;
			if(r.match(i)) {
				neko.FileSystem.deleteFile(p+"/"+i);
			}
			else {
				if(neko.FileSystem.isDirectory(p+"/"+i) && recurse == false)
					continue;
				neko.Lib.println("ERROR: There is an unexpected file in the "+ p +" directory.");
				neko.Lib.println(p+"/"+i);
				neko.Sys.exit(1);
			}
		}
	}

	static function copyBuildToTarget(p:String) {
		var dir = BUILDROOT + "/" + p;
		var files = neko.FileSystem.readDirectory(dir);
		for(i in files) {
			if(neko.FileSystem.isDirectory(dir+"/"+i)) {
				if(i.charAt(0) == ".") {
					continue;
				}
				var dest : String = TARGETROOT+"/zones/"+p+"/"+i;
				dest = StringTools.replace(dest,"//", "/");
				neko.Lib.println("Create directory "+dest);
				if(!neko.FileSystem.isDirectory(dest))
					neko.FileSystem.createDirectory(dest);
				copyBuildToTarget(p+"/"+i);
				continue;
			}
			var r = ~/\.hx$/;
			if(r.match(i)) {
				var dest : String;
				if(i != "Timezone.hx")
					dest = TARGETROOT + "/zones/" + p + "/" + i;
				else
					dest = TARGETROOT + "/" + i;
				var source : String = dir + "/" + i;
				// just in case, compatible?
				dest = StringTools.replace(dest,"//", "/");
				source = StringTools.replace(source,"//", "/");
				neko.Lib.println("Installing " + source + " to " + dest);
				neko.io.File.copy(source, dest);
			}
			else {
				neko.Lib.println("ERROR: There is an unexpected file in the "+ p +" directory.");
				neko.Lib.println(p+"/"+i);
				neko.Sys.exit(1);
			}
		}
		for(i in STATICS) {
			neko.io.File.copy(STATICSROOT + "/" + i, TARGETROOT + "/" + i);
		}
	}

	static function cleanup() : Void {
		emptyDirectory(BUILDROOT,true);
		try {
			neko.FileSystem.deleteDirectory(BUILDROOT);
		}
		catch(e:Dynamic) {
			neko.Lib.println("Unable to remove the temporary build directory "+BUILDROOT);
		}
	}

	static function install() : Void {
		var files = neko.FileSystem.readDirectory(BUILDROOT);
		for(i in files) {
			trace(i);
		}
	}

	public static function getInstance() : Parser {
		return _instance;
	}



	public function new() {
		Parser._instance = this;
	}
	public function parseFile(s : String) : {r: Bool, s:Array<String> } {
		var fi = neko.io.File.read("./tzparser/data/" + s, false);
		rules = new Hash();
		aliases = new Hash<Array<AliasZone>>();
		zones = new Hash();
		try {
			while(true) {
				var line = StringTools.trim(fi.readLine());
				if(line.length == 0 || line.charAt(0) == '#')
					continue;
				trace(line);
				switch(line.charAt(0)) {
				case 'R':
					// parse Rule
					//state = RULE;
					// TODO: parse rule line
					var rl : Rule = ParseRule.parse(line);
					if(rl == null || rl.name.length == 0) {
						trace("Cannot parse rule from line:\n"+line);
						return {r: false, s: null};
					}
					if(!rules.exists(rl.name)) {
						rules.set(rl.name, new RuleSet(rl.name));
					}
					rules.get(rl.name).add(rl);
					continue;
				case 'Z':
					// parse Zone
					//state = ZONE;
					// Zone lines can continue, so the file handle is passed
					var z = ParseZone.parse(line, fi);
					zones.set(z.name,z);
					trace("Parser.parseFile: Parsed zone " +z.name + "\n" + z.rules);
					continue;
				case 'L':
					if(line.charAt(1) == 'i') {
						var a : AliasZone = LinkLine.parse(line);
						if(!aliases.exists(a.name)) {
							aliases.set(a.name, new Array<AliasZone>());
						}
						aliases.get(a.name).push(a);
					}
					else {
						// Leap
						// TODO: ignored
						continue;
					}
					continue;
				}
				trace("Unhandled line in file " + s);
				trace(line);
				return {r: false, s: null};
			}
		}
		catch(e : haxe.io.Eof) { trace("EOF");}
		catch(e : Dynamic) {
			neko.Lib.println(e);
			trace(haxe.Stack.exceptionStack());
			return {r: false, s: null};
		}
		for(i in rules.keys()) {
			rules.get(i).sort();
		}
		for(i in zones) {
			i.finalize();
		}
// 		if(s == "northamerica") {
// 			trace(zones.get("America/New_York"));
// 			trace(rules);
// 		}
		var sb = new Array<String>();
		for(i in zones) {
			//trace(i.name + " " + i.className + " " + i.packageName);
			sb.push('zones.set("' + i.name + '", new ' + i.packageName + '.' + i.className + '());');
			var path = packageNameToPath(i.packageName);
			var fname = path + "/" + i.className + ".hx";

			if(!neko.FileSystem.exists(path)) {
				neko.Lib.println("Creating directory "+ path);
				neko.FileSystem.createDirectory(path);
			}
			if(neko.FileSystem.exists(fname)) {
				neko.Lib.println("ERROR: Naming collision on "+ fname + " zone.");
				neko.Sys.exit(0);
			}
			neko.Lib.println("Writing "+fname);
			var fo = neko.io.File.write(fname,false);
			fo.write(Bytes.ofString(i.toString()));
			fo.close();
		}

		// Hash<Array<AliasZone>>
		for(j in aliases.keys()) {
			for(i in aliases.get(j)) {

				sb.push('zones.set("' + i.alias + '", new ' + i.aliasPackageName + '.' + i.aliasClassName + '()); // alias');
				var path = packageNameToPath(i.aliasPackageName);
				var fname = path + "/" + i.aliasClassName + ".hx";
				if(!neko.FileSystem.exists(path)) {
					neko.Lib.println("Creating directory "+ path + " for alias.");
					neko.FileSystem.createDirectory(path);
				}
				if(neko.FileSystem.exists(fname)) {
					neko.Lib.println("ERROR: Naming collision on "+ fname + " for alias.");
					neko.Sys.exit(0);
				}
				neko.Lib.println("Writing "+fname);
				var fo = neko.io.File.write(fname,false);
				fo.write(Bytes.ofString(i.toString()));
				fo.close();
			}
		}

		return {r: true, s: sb };
	}

	public static function zoneNameToClassVars(name:String)
		: { classname: String, packagename: String }
	{
		var parts : Array<String>= name.split("/");
		var className = parts.pop();
		className = StringTools.replace(className,"-","_minus_");
		className = StringTools.replace(className,"+","_plus_");

		var it = parts.iterator();
		var sb = new StringBuf();
		sb.add("tz.zones.");
		for(i in it) {
			sb.add(i.toLowerCase());
			if(it.hasNext())
				sb.add(".");
		}
		var packageName = sb.toString();
		if(packageName.charAt(packageName.length-1) == ".")
			packageName = packageName.substr(0,packageName.length-1);

		return {classname: className, packagename: packageName};
	}

	public static function packageNameToPath(s : String) : String {
		var path = BUILDROOT + "/" + StringTools.replace(s.substr(9),".","/");
		var e = ~/\/\//g;
		path = e.replace(path,"/");
		e = ~/\/$/g;
		path = e.replace(path,"");
		return path;
	}

	public static function quotedString(s : String) : String {
		var sb = new StringBuf();
		sb.add('"');
		sb.add(StringTools.replace(s, '"', '\"'));
		sb.add('"');
		return sb.toString();
	}

static var TimeZoneHeader : String =
"
// THIS FILE is automatically generated by tzparser. DO NOT MODIFY
/* ************************************************************************ */
/*                                                                          */
/*  Copyright (c)2005-2012 Russell Weir | damonsbane _at_ gmail             */
/*                                                                          */
/* This library is free software; you can redistribute it and/or            */
/* modify it under the terms of the GNU Lesser General Public               */
/* License as published by the Free Software Foundation; either             */
/* version 3.0 of the License, or (at your option) any later version.       */
/*                                                                          */
/* This library is distributed in the hope that it will be useful,          */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of           */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        */
/* Lesser General Public License or the LICENSE file for more details.      */
/*                                                                          */
/* ************************************************************************ */

/**
	tz = tz.Timezone.get('Africa/Algiers');
	local = tz.utcToLocal(new Gregorian(2007,7,26,16,46,0));
*/
package tz;

class Timezone {
	static var initialized : Bool	= false;
	static var aliases : Hash<String>;
	static var zones : Hash<Dynamic>;

	static function initialize() : Void {
		aliases = new Hash<String>();
		zones = new Hash<Dynamic>();
";


static var TimeZoneFooter : String =
"
	/**
		Get a zone by it's English name, for example
		America/New_York or Mexico/BajaSur.
		By using this method, all timezone information
		will be compiled. This is over 1.5MB of data,
		so you can either take a copy of Timezone.hx and
		truncate it, or select individual timezones by
		instancing them.
		You can find all the English names, and the
		corresponding class names, by reading this source
		file.
	*/
	public static function get(zone:String) : tz.Zone {
		if(!initialized)
			initialize();
		if(aliases.exists(zone))
			zone = aliases.get(zone);
		else if(!zones.exists(zone))
			throw 'Timezone '+ zone + ' does not exist';
		return zones.get(zone);
	}
}
";


}
#else error
#end
